#include <algorithm>

#include "OBS ESP.h"

#include "Options.hpp"
#include "Utils/Math.h"
#include "Utils/Utils.h"
#include "SDK/CEntity.h"
#include "SDK/Hitboxes.h"
#include "SDK/CGlobalVarsBase.h"
#include "SDK/IVModelInfo.h"
#define g_LocalPlayer g_pEntityList->GetClientEntity(g_pEngine->GetLocalPlayer())

RECT GetBBox(C_BaseEntity* ent)
{
    RECT rect{};
    auto collideable = ent->GetCollideable();

    if (!collideable)
        return rect;

    auto min = collideable->OBBMins();
    auto max = collideable->OBBMaxs();

    const matrix3x4_t& trans = ent->m_rgflCoordinateFrame();

    Vector points[] = {
        Vector(min.x, min.y, min.z),
        Vector(min.x, max.y, min.z),
        Vector(max.x, max.y, min.z),
        Vector(max.x, min.y, min.z),
        Vector(max.x, max.y, max.z),
        Vector(min.x, max.y, max.z),
        Vector(min.x, min.y, max.z),
        Vector(max.x, min.y, max.z)
    };

    Vector pointsTransformed[8];
    for (int i = 0; i < 8; i++) {
        g_Math.VectorTransform(points[i], trans, pointsTransformed[i]);
    }

    Vector screen_points[8] = {};

    for (int i = 0; i < 8; i++) {
        if (!Utils::WorldToScreen(pointsTransformed[i], screen_points[i]))
            return rect;
    }

    auto left = screen_points[0].x;
    auto top = screen_points[0].y;
    auto right = screen_points[0].x;
    auto bottom = screen_points[0].y;

    for (int i = 1; i < 8; i++) {
        if (left > screen_points[i].x)
            left = screen_points[i].x;
        if (top < screen_points[i].y)
            top = screen_points[i].y;
        if (right < screen_points[i].x)
            right = screen_points[i].x;
        if (bottom > screen_points[i].y)
            bottom = screen_points[i].y;
    }
    return RECT{ (long)left, (long)top, (long)right, (long)bottom };
}

Visuals::Visuals()
{
    InitializeCriticalSection(&cs);
}

Visuals::~Visuals() {
    DeleteCriticalSection(&cs);
}

//--------------------------------------------------------------------------------
void Visuals::Render() {
}
//--------------------------------------------------------------------------------
bool Visuals::Player::Begin(C_BaseEntity* pl)
{
    if (pl->IsDormant() || !pl->IsAlive())
        return false;

    ctx.pl = pl;
    ctx.is_enemy = g_LocalPlayer->GetTeam() != pl->GetTeam();
    //ctx.is_visible = g_LocalPlayer->CanSeePlayer(pl, HITBOX_CHEST);

    if (!ctx.is_enemy && g_Options.esp_enemies_only)
        return false;

    ctx.clr = g_Options.color_esp_enemy_visible;//ctx.is_enemy ? (ctx.is_visible ? g_Options.color_esp_enemy_visible : g_Options.color_esp_enemy_occluded) : (ctx.is_visible ? g_Options.color_esp_ally_visible : g_Options.color_esp_ally_occluded);

    auto head = pl->hitbox_position(HITBOX_HEAD);
    auto origin = pl->GetOrigin();

    head.z += 15;

    if (!Utils::WorldToScreen(head, ctx.head_pos) ||
        !Utils::WorldToScreen(origin, ctx.feet_pos))
        return false;

    auto h = fabs(ctx.head_pos.y - ctx.feet_pos.y);
    auto w = h / 1.65f;

    ctx.bbox.left = static_cast<long>(ctx.feet_pos.x - w * 0.5f);
    ctx.bbox.right = static_cast<long>(ctx.bbox.left + w);
    ctx.bbox.bottom = static_cast<long>(ctx.feet_pos.y);
    ctx.bbox.top = static_cast<long>(ctx.head_pos.y);

    return true;
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderBox() {
    Render::get().RenderBoxByType(ctx.bbox.left, ctx.bbox.top, ctx.bbox.right, ctx.bbox.bottom, ctx.clr, 1);
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderName()
{
    PlayerInfo_t info = ctx.pl->GetPlayerInfo();

    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, info.szName);

    Render::get().RenderText(info.szName, ctx.feet_pos.x - sz.x / 2, ctx.head_pos.y - sz.y, 14.f, ctx.clr);
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderHealth()
{
    auto  hp = ctx.pl->GetHealth();
    float box_h = (float)fabs(ctx.bbox.bottom - ctx.bbox.top);
    //float off = (box_h / 6.f) + 5;
    float off = 8;

    int height = (box_h * hp) / 100;

    int green = int(hp * 2.55f);
    int red = 255 - green;

    int x = ctx.bbox.left - off;
    int y = ctx.bbox.top;
    int w = 4;
    int h = box_h;

    Render::get().RenderBox(x, y, x + w, y + h, Color(0, 0, 0), 1.f, true);
    Render::get().RenderBox(x + 1, y + 1, x + w - 1, y + height - 2, Color(red, green, 0, 255), 1.f, true);
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderArmour()
{
    auto  armour = ctx.pl->ArmorValue();
    float box_h = (float)fabs(ctx.bbox.bottom - ctx.bbox.top);
    //float off = (box_h / 6.f) + 5;
    float off = 4;

    int height = (((box_h * armour) / 100));

    int x = ctx.bbox.right + off;
    int y = ctx.bbox.top;
    int w = 4;
    int h = box_h;

    Render::get().RenderBox(x, y, x + w, y + h, Color(0, 0, 0), 1.f, true);
    Render::get().RenderBox(x + 1, y + 1, x + w - 1, y + height - 2, Color(0, 50, 255, 255), 1.f, true);
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderWeaponName()
{
    auto weapon = ctx.pl->GetActiveWeapon();

    if (!weapon) return;

    auto text = weapon->GetCSWpnData()->weapon_name + 7;
    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, text);
    Render::get().RenderText(text, ctx.feet_pos.x, ctx.feet_pos.y, 14.f, ctx.clr, true,
        g_pDefaultFont);
}
//--------------------------------------------------------------------------------
void Visuals::Player::RenderSnapline()
{

    int screen_w, screen_h;
    g_pEngine->GetScreenSize(screen_w, screen_h);

    Render::get().RenderLine(screen_w / 2.f, (float)screen_h,
        ctx.feet_pos.x, ctx.feet_pos.y, ctx.clr);
}
//--------------------------------------------------------------------------------
void Visuals::RenderCrosshair()
{
    int w, h;

    g_pEngine->GetScreenSize(w, h);

    int cx = w / 2;
    int cy = h / 2;
    Render::get().RenderLine(cx - 25, cy, cx + 25, cy, g_Options.color_esp_crosshair);
    Render::get().RenderLine(cx, cy - 25, cx, cy + 25, g_Options.color_esp_crosshair);
}
//--------------------------------------------------------------------------------
void Visuals::RenderWeapon(C_BaseCombatWeapon* ent)
{
    auto clean_item_name = [](const char* name) -> const char* {
        if (name[0] == 'C')
            name++;

        auto start = strstr(name, "Weapon");
        if (start != nullptr)
            name = start + 6;

        return name;
    };

    // We don't want to Render weapons that are being held
    if (ent->owner().IsValid())
        return;

    auto bbox = GetBBox(ent);

    if (bbox.right == 0 || bbox.bottom == 0)
        return;

    Render::get().RenderBox(bbox, g_Options.color_esp_weapons);


    auto name = clean_item_name(ent->GetClientClass()->pNetworkName);

    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, name);
    int w = bbox.right - bbox.left;


    Render::get().RenderText(name, ImVec2((bbox.left + w * 0.5f) - sz.x * 0.5f, bbox.bottom + 1), 14.f, g_Options.color_esp_weapons);
}
//--------------------------------------------------------------------------------
void Visuals::RenderDefuseKit(C_BaseEntity* ent)
{
    if (ent->owner().IsValid())
        return;

    auto bbox = GetBBox(ent);

    if (bbox.right == 0 || bbox.bottom == 0)
        return;

    Render::get().RenderBox(bbox, g_Options.color_esp_defuse);

    auto name = "Defuse Kit";
    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, name);
    int w = bbox.right - bbox.left;
    Render::get().RenderText(name, ImVec2((bbox.left + w * 0.5f) - sz.x * 0.5f, bbox.bottom + 1), 14.f, g_Options.color_esp_defuse);
}
//--------------------------------------------------------------------------------
void Visuals::RenderPlantedC4(C_BaseEntity* ent)
{
    auto bbox = GetBBox(ent);

    if (bbox.right == 0 || bbox.bottom == 0)
        return;


    Render::get().RenderBox(bbox, g_Options.color_esp_c4);


    int bombTimer = std::ceil(ent->m_flC4Blow() - g_pGlobalVars->curtime);
    std::string timer = std::to_string(bombTimer);

    auto name = (bombTimer < 0.f) ? "Bomb" : timer;
    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, name.c_str());
    int w = bbox.right - bbox.left;

    Render::get().RenderText(name, ImVec2((bbox.left + w * 0.5f) - sz.x * 0.5f, bbox.bottom + 1), 14.f, g_Options.color_esp_c4);
}
//--------------------------------------------------------------------------------
void Visuals::RenderItemEsp(C_BaseEntity* ent)
{
    std::string itemstr = "Undefined";
    const model_t* itemModel = ent->GetModel();
    if (!itemModel)
        return;
    studiohdr_t* hdr = g_pModelInfo->GetStudiomodel(itemModel);
    if (!hdr)
        return;
    itemstr = hdr->name;
    if (ent->GetClientClass()->ClassID == ClassId::CBumpMine)
        itemstr = "";
    else if (itemstr.find("case_pistol") != std::string::npos)
        itemstr = "Pistol Case";
    else if (itemstr.find("case_light_weapon") != std::string::npos)
        itemstr = "Light Case";
    else if (itemstr.find("case_heavy_weapon") != std::string::npos)
        itemstr = "Heavy Case";
    else if (itemstr.find("case_explosive") != std::string::npos)
        itemstr = "Explosive Case";
    else if (itemstr.find("case_tools") != std::string::npos)
        itemstr = "Tools Case";
    else if (itemstr.find("random") != std::string::npos)
        itemstr = "Airdrop";
    else if (itemstr.find("dz_armor_helmet") != std::string::npos)
        itemstr = "Full Armor";
    else if (itemstr.find("dz_helmet") != std::string::npos)
        itemstr = "Helmet";
    else if (itemstr.find("dz_armor") != std::string::npos)
        itemstr = "Armor";
    else if (itemstr.find("upgrade_tablet") != std::string::npos)
        itemstr = "Tablet Upgrade";
    else if (itemstr.find("briefcase") != std::string::npos)
        itemstr = "Briefcase";
    else if (itemstr.find("parachutepack") != std::string::npos)
        itemstr = "Parachute";
    else if (itemstr.find("dufflebag") != std::string::npos)
        itemstr = "Cash Dufflebag";
    else if (itemstr.find("ammobox") != std::string::npos)
        itemstr = "Ammobox";
    else if (itemstr.find("dronegun") != std::string::npos)
        itemstr = "Turrel";
    else if (itemstr.find("exojump") != std::string::npos)
        itemstr = "Exojump";
    else if (itemstr.find("healthshot") != std::string::npos)
        itemstr = "Healthshot";
    else {
        /*May be you will search some missing items..*/
        /*static std::vector<std::string> unk_loot;
        if (std::find(unk_loot.begin(), unk_loot.end(), itemstr) == unk_loot.end()) {
            Utils::ConsolePrint(itemstr.c_str());
            unk_loot.push_back(itemstr);
        }*/
        return;
    }

    auto bbox = GetBBox(ent);
    if (bbox.right == 0 || bbox.bottom == 0)
        return;
    auto sz = g_pDefaultFont->CalcTextSizeA(14.f, FLT_MAX, 0.0f, itemstr.c_str());
    int w = bbox.right - bbox.left;


    //Render::Get().RenderBox(bbox, g_Options.color_esp_item);
    Render::get().RenderText(itemstr, ImVec2((bbox.left + w * 0.5f) - sz.x * 0.5f, bbox.bottom + 1), 14.f, g_Options.color_esp_item);
}
//--------------------------------------------------------------------------------
void Visuals::ThirdPerson() {
    if (!g_LocalPlayer)
        return;

    /*if (g_Options.misc_thirdperson && g_LocalPlayer->IsAlive())
     {
         if (!g_Input->camera_in_third_person)
         {
             g_Input->camera_in_third_person = true;
         }

         float dist = g_Options.misc_thirdperson_dist;

         QAngle* view = g_LocalPlayer->GetVAngles();
         trace_t tr;
         Ray_t ray;

         Vector desiredCamOffset = Vector(cos(DEG2RAD(view->yaw)) * dist,
             sin(DEG2RAD(view->yaw)) * dist,
             sin(DEG2RAD(-view->pitch)) * dist
         );

         //cast a ray from the Current camera Origin to the Desired 3rd person Camera origin
         ray.Init(g_LocalPlayer->GetEyePos(), (g_LocalPlayer->GetEyePos() - desiredCamOffset));
         CTraceFilter traceFilter;
         traceFilter.pSkip = g_LocalPlayer;
         g_EngineTrace->TraceRay(ray, MASK_SHOT, &traceFilter, &tr);

         Vector diff = g_LocalPlayer->GetEyePos() - tr.endpos;

         float distance2D = sqrt(abs(diff.x * diff.x) + abs(diff.y * diff.y));// Pythagorean

         bool horOK = distance2D > (dist - 2.0f);
         bool vertOK = (abs(diff.z) - abs(desiredCamOffset.z) < 3.0f);

         float cameraDistance;

         if (horOK && vertOK)  // If we are clear of obstacles
         {
             cameraDistance = dist; // go ahead and set the distance to the setting
         }
         else
         {
             if (vertOK) // if the Vertical Axis is OK
             {
                 cameraDistance = distance2D * 0.95f;
             }
             else// otherwise we need to move closer to not go into the floor/ceiling
             {
                 cameraDistance = abs(diff.z) * 0.95f;
             }
         }
         g_Input->m_fCameraInThirdPerson = true;

         g_Input->m_vecCameraOffset.z = cameraDistance;
     }
     else
     {
         g_Input->m_fCameraInThirdPerson = false;
     }*/
}


void Visuals::AddToDrawList() {
    for (auto i = 1; i <= g_pEntityList->GetHighestEntityIndex(); ++i) {
        auto entity = C_BaseEntity::GetEntityByIndex(i);

        if (!entity)
            continue;

        if (entity == g_LocalPlayer && !g_Input->m_fCameraInThirdPerson)
            continue;

        if (i <= g_pGlobalVars->maxClients) {
            auto player = Player();
            if (player.Begin((C_BaseEntity*)entity)) {
                if (g_Options.esp_player_snaplines) player.RenderSnapline();
                if (g_Options.esp_player_boxes)     player.RenderBox();
                if (g_Options.esp_player_weapons)   player.RenderWeaponName();
                if (g_Options.esp_player_names)     player.RenderName();
                if (g_Options.esp_player_health)    player.RenderHealth();
                if (g_Options.esp_player_armour)    player.RenderArmour();
            }
        }
        else if (g_Options.esp_dropped_weapons && entity->IsWeapon())
            RenderWeapon(static_cast<C_BaseCombatWeapon*>(entity));
        else if (g_Options.esp_dropped_weapons && entity->IsDefuseKit())
            RenderDefuseKit(entity);
        else if (entity->IsPlantedC4() && g_Options.esp_planted_c4)
            RenderPlantedC4(entity);
        else if (entity->IsLoot() && g_Options.esp_items)
            RenderItemEsp(entity);
    }


    if (g_Options.esp_crosshair)
        RenderCrosshair();

}
